using System.Runtime.CompilerServices;
using System.Collections.Generic;

//
// * Copyright (c) 2002-2009 "Neo Technology,"
// *     Network Engine for Objects in Lund AB [http://neotechnology.com]
// *
// * This file is part of Neo4j.
// * 
// * Neo4j is free software: you can redistribute it and/or modify
// * it under the terms of the GNU Affero General Public License as
// * published by the Free Software Foundation, either version 3 of the
// * License, or (at your option) any later version.
// * 
// * This program is distributed in the hope that it will be useful,
// * but WITHOUT ANY WARRANTY; without even the implied warranty of
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// * GNU Affero General Public License for more details.
// * 
// * You should have received a copy of the GNU Affero General Public License
// * along with this program. If not, see <http://www.gnu.org/licenses/>.
// 
namespace org.neo4j.kernel.impl.transaction.xaframework
{

	using XAException = javax.transaction.xa.XAException;
	using XAResource = javax.transaction.xa.XAResource;
	using Xid = javax.transaction.xa.Xid;

	using ArrayMap = org.neo4j.kernel.impl.util.ArrayMap;

// make package access?
	public class XaResourceManager
	{
		private readonly ArrayMap<XAResource, Xid> xaResourceMap = new ArrayMap<XAResource, Xid>();
		private readonly ArrayMap<Xid, XidStatus> xidMap = new ArrayMap<Xid, XidStatus>();
		private int recoveredTxCount = 0;

		private XaLogicalLog log = null;
		private readonly XaTransactionFactory tf;
		private readonly string name;

		internal XaResourceManager(XaTransactionFactory tf, string name)
		{
			this.tf = tf;
			this.name = name;
		}

		[MethodImpl(MethodImplOptions.Synchronized)]
		internal virtual void setLogicalLog(XaLogicalLog log)
		{
			this.log = log;
		}

		lock XaTransaction getXaTransaction(XAResource xaRes)
			throws XAException;
		{
			XidStatus status = xidMap.get(xaResourceMap.get(xaRes));
			if (status == null)
			{
				throw new XAException("Resource[" + xaRes + "] not enlisted");
			}
			return status.getTransactionStatus().getTransaction();
		}

		lock void Start(XAResource xaResource, Xid xid)
			throws XAException;
		{
			if (xaResourceMap.get(xaResource) != null)
			{
				throw new XAException("Resource[" + xaResource + "] already enlisted or suspended");
			}
			xaResourceMap.put(xaResource, xid);
			if (xidMap.get(xid) == null)
			{
				int identifier = log.Start(xid);
				XaTransaction xaTx = tf.create(identifier);
				xidMap.put(xid, new XidStatus(xaTx));
			}
		}

		lock void injectStart(Xid xid, XaTransaction tx)
			throws IOException;
		{
			if (xidMap.get(xid) != null)
			{
				throw new IOException("Inject start failed, xid: " + xid + " already injected");
			}
			xidMap.put(xid, new XidStatus(tx));
			recoveredTxCount++;
		}

		lock void resume(Xid xid)
			throws XAException;
		{
			XidStatus status = xidMap.get(xid);
			if (status == null)
			{
				throw new XAException("Unknown xid[" + xid + "]");
			}

			if (status.getActive())
			{
				throw new XAException("Xid [" + xid + "] not suspended");
			}
			status.setActive(true);
		}

		lock void join(XAResource xaResource, Xid xid)
			throws XAException;
		{
			if (xidMap.get(xid) == null)
			{
				throw new XAException("Unknown xid[" + xid + "]");
			}
			if (xaResourceMap.get(xaResource) != null)
			{
				throw new XAException("Resource[" + xaResource + "] already enlisted");
			}
			xaResourceMap.put(xaResource, xid);
		}

		lock void end(XAResource xaResource, Xid xid)
			throws XAException;
		{
			Xid xidEntry = xaResourceMap.Remove(xaResource);
			if (xidEntry == null)
			{
				throw new XAException("Resource[" + xaResource + "] not enlisted");
			}
		}

		lock void suspend(Xid xid)
			throws XAException;
		{
			XidStatus status = xidMap.get(xid);
			if (status == null)
			{
				throw new XAException("Unknown xid[" + xid + "]");
			}
			if (!status.getActive())
			{
				throw new XAException("Xid[" + xid + "] already suspended");
			}
			status.setActive(@false);
		}

		lock void fail(XAResource xaResource, Xid xid)
			throws XAException;
		{
			if (xidMap.get(xid) == null)
			{
				throw new XAException("Unknown xid[" + xid + "]");
			}
			Xid xidEntry = xaResourceMap.Remove(xaResource);
			if (xidEntry == null)
			{
				throw new XAException("Resource[" + xaResource + "] not enlisted");
			}
			XidStatus status = xidMap.get(xid);
			status.getTransactionStatus().markAsRollback();
		}

		lock void validate(XAResource xaResource)
			throws XAException;
		{
			XidStatus status = xidMap.get(xaResourceMap.get(xaResource));
			if (status == null)
			{
				throw new XAException("Resource[" + xaResource + "] not enlisted");
			}
			if (!status.getActive())
			{
				throw new XAException("Resource[" + xaResource + "] suspended");
			}
		}

	// TODO: check so we're not currently committing on the resource
		lock void destroy(XAResource xaResource)
		{
			xaResourceMap.Remove(xaResource);
		}

		private static class XidStatus
		{
			private bool active = true;
			private TransactionStatus txStatus;

			XidStatus(XaTransaction xaTransaction)
			{
				txStatus = new TransactionStatus(xaTransaction);
			}

			void setActive(bool active)
			{
				this.active = active;
			}

			bool getActive()
			{
				return this.active;
			}

			TransactionStatus getTransactionStatus()
			{
				return txStatus;
			}
		}

		private static class TransactionStatus
		{
			private bool prepared = @false;
			private bool commitStarted = @false;
			private bool rollback = @false;
			private final XaTransaction xaTransaction;

			TransactionStatus(XaTransaction xaTransaction)
			{
				this.xaTransaction = xaTransaction;
			}

			void markAsPrepared()
			{
				prepared = true;
			}

			void markAsRollback()
			{
				rollback = true;
			}

			void markCommitStarted()
			{
				commitStarted = true;
			}

			bool prepared()
			{
				return prepared;
			}

			bool rollback()
			{
				return rollback;
			}

			bool commitStarted()
			{
				return commitStarted;
			}

			XaTransaction getTransaction()
			{
				return xaTransaction;
			}

			public string ToString()
			{
				return "TransactionStatus[" + xaTransaction.getIdentifier() + ", prepared=" + prepared + ", commitStarted=" + commitStarted + ", rolledback=" + rollback + "]";
			}
		}

		lock int prepare(Xid xid)
			throws XAException;
		{
			XidStatus status = xidMap.get(xid);
			if (status == null)
			{
				throw new XAException("Unknown xid[" + xid + "]");
			}
			TransactionStatus txStatus = status.getTransactionStatus();
			XaTransaction xaTransaction = txStatus.getTransaction();
			if (xaTransaction.isReadOnly())
			{
				log.done(xaTransaction.getIdentifier());
				xidMap.Remove(xid);
				if (xaTransaction.isRecovered())
				{
					recoveredTxCount--;
					checkIfRecoveryComplete();
				}
				return XAResource.XA_RDONLY;
			}
			else
			{
				xaTransaction.prepare();
				log.prepare(xaTransaction.getIdentifier());
				txStatus.markAsPrepared();
				return XAResource.XA_OK;
			}
		}

	// called from XaResource internal recovery
	// returns true if read only and should be removed...
		lock bool injectPrepare(Xid xid)
			throws IOException;
		{
			XidStatus status = xidMap.get(xid);
			if (status == null)
			{
				throw new IOException("Unknown xid[" + xid + "]");
			}
			TransactionStatus txStatus = status.getTransactionStatus();
			XaTransaction xaTransaction = txStatus.getTransaction();
			if (xaTransaction.isReadOnly())
			{
				xidMap.Remove(xid);
				if (xaTransaction.isRecovered())
				{
					recoveredTxCount--;
					checkIfRecoveryComplete();
				}
				return true;
			}
			else
			{
				txOrderMap.put(xid, nextTxOrder++);
				txStatus.markAsPrepared();
				return @false;
			}
		}

		private Map<Xid, int> txOrderMap = new Dictionary<Xid, int>();
		private int nextTxOrder = 0;

	// called during recovery
	// if not read only transaction will be commited.
		lock void injectOnePhaseCommit(Xid xid)
			throws XAException;
		{
			XidStatus status = xidMap.get(xid);
			if (status == null)
			{
				throw new XAException("Unknown xid[" + xid + "]");
			}
			TransactionStatus txStatus = status.getTransactionStatus();
			txOrderMap.put(xid, nextTxOrder++);
			txStatus.markAsPrepared();
			txStatus.markCommitStarted();
			XaTransaction xaTransaction = txStatus.getTransaction();
			xaTransaction.commit();
		}

		lock void injectTwoPhaseCommit(Xid xid)
			throws XAException;
		{
			XidStatus status = xidMap.get(xid);
			if (status == null)
			{
				throw new XAException("Unknown xid[" + xid + "]");
			}
			TransactionStatus txStatus = status.getTransactionStatus();
			txOrderMap.put(xid, nextTxOrder++);
			txStatus.markAsPrepared();
			txStatus.markCommitStarted();
			XaTransaction xaTransaction = txStatus.getTransaction();
			xaTransaction.commit();
		}

		lock XaTransaction commit(Xid xid, bool onePhase)
			throws XAException;
		{
			XidStatus status = xidMap.get(xid);
			if (status == null)
			{
				throw new XAException("Unknown xid[" + xid + "]");
			}
			TransactionStatus txStatus = status.getTransactionStatus();
			XaTransaction xaTransaction = txStatus.getTransaction();
			if (onePhase)
			{
				if (!xaTransaction.isReadOnly())
				{
					if (!xaTransaction.isRecovered())
					{
						xaTransaction.prepare();
						log.commitOnePhase(xaTransaction.getIdentifier());
					}
				}
				txStatus.markAsPrepared();
			}
			if (!txStatus.prepared() || txStatus.rollback())
			{
				throw new XAException("Transaction not prepared or " + "(marked as) rolledbacked");
			}
			if (!xaTransaction.isReadOnly())
			{
				if (!xaTransaction.isRecovered())
				{
					if (!onePhase)
					{
						log.commitTwoPhase(xaTransaction.getIdentifier());
					}
				}
				txStatus.markCommitStarted();
				xaTransaction.commit();
			}
			log.done(xaTransaction.getIdentifier());
			xidMap.Remove(xid);
			if (xaTransaction.isRecovered())
			{
				recoveredTxCount--;
				checkIfRecoveryComplete();
			}
			return xaTransaction;
		}

		lock XaTransaction rollback(Xid xid)
			throws XAException;
		{
			XidStatus status = xidMap.get(xid);
			if (status == null)
			{
				throw new XAException("Unknown xid[" + xid + "]");
			}
			TransactionStatus txStatus = status.getTransactionStatus();
			XaTransaction xaTransaction = txStatus.getTransaction();
			if (txStatus.commitStarted())
			{
				throw new XAException("Transaction already started commit");
			}
			txStatus.markAsRollback();
			xaTransaction.rollback();
			log.done(xaTransaction.getIdentifier());
			xidMap.Remove(xid);
			if (xaTransaction.isRecovered())
			{
				recoveredTxCount--;
				checkIfRecoveryComplete();
			}
			return txStatus.getTransaction();
		}

		lock XaTransaction forget(Xid xid)
			throws XAException;
		{
			XidStatus status = xidMap.get(xid);
			TransactionStatus txStatus = status.getTransactionStatus();
			XaTransaction xaTransaction = txStatus.getTransaction();
			log.done(xaTransaction.getIdentifier());
			xidMap.Remove(xid);
			if (xaTransaction.isRecovered())
			{
				recoveredTxCount--;
				checkIfRecoveryComplete();
			}
			return xaTransaction;
		}

		lock void markAsRollbackOnly(Xid xid)
			throws XAException;
		{
			XidStatus status = xidMap.get(xid);
			if (status == null)
			{
				throw new XAException("Unknown xid[" + xid + "]");
			}
			TransactionStatus txStatus = status.getTransactionStatus();
			txStatus.markAsRollback();
		}

		lock Xid[] recover(int flag)
			throws XAException;
		{
			List<Xid> xids = new List<Xid>();
			IEnumerator <Xid> keyIterator = xidMap.Keys.GetEnumerator();
			while (keyIterator.MoveNext())
			{
				xids.Add(keyIterator.Current);
			}
			return xids.ToArray(new Xid[xids.Count]);
		}

	// called from neostore internal recovery
		lock void pruneXid(Xid xid)
			throws IOException;
		{
			XidStatus status = xidMap.get(xid);
			if (status == null)
			{
				throw new IOException("Unknown xid[" + xid + "]");
			}
			TransactionStatus txStatus = status.getTransactionStatus();
			XaTransaction xaTransaction = txStatus.getTransaction();
			xidMap.Remove(xid);
			if (xaTransaction.isRecovered())
			{
				recoveredTxCount--;
				checkIfRecoveryComplete();
			}
		}

		lock void pruneXidIfExist(Xid xid)
			throws IOException;
		{
			XidStatus status = xidMap.get(xid);
			if (status == null)
			{
				return;
			}
			TransactionStatus txStatus = status.getTransactionStatus();
			XaTransaction xaTransaction = txStatus.getTransaction();
			xidMap.Remove(xid);
			if (xaTransaction.isRecovered())
			{
				recoveredTxCount--;
				checkIfRecoveryComplete();
			}
		}

		lock void checkXids()
			throws IOException;
		{
			IEnumerator <Xid> keyIterator = xidMap.Keys.GetEnumerator();
			LinkedList<Xid> xids = new LinkedList<Xid>();
			while (keyIterator.MoveNext())
			{
				xids.Add(keyIterator.Current);
			}
		// comparator only used here
			Collections.sort(xids, new Comparator<Xid>() { public int compare(Xid o1, Xid o2) { int id1 = txOrderMap.get(o1); int id2 = txOrderMap.get(o2); if (id1 == null && id2 == null) { return 0; } if (id1 == null) { return int.MaxValue; } if (id2 == null) { return int.MinValue; } return id1 - id2; } });
			txOrderMap = null;
			Logger logger = Logger.getLogger(tf.GetType().Name);
			while (!xids.Count == 0)
			{
				Xid xid = xids.RemoveFirst();
				XidStatus status = xidMap.get(xid);
				TransactionStatus txStatus = status.getTransactionStatus();
				XaTransaction xaTransaction = txStatus.getTransaction();
				int identifier = xaTransaction.getIdentifier();
				if (xaTransaction.isRecovered())
				{
					if (txStatus.commitStarted())
					{
						logger.fine("Marking 1PC [" + name + "] tx " + identifier + " as done");
						log.doneInternal(identifier);
						xidMap.Remove(xid);
						recoveredTxCount--;
					}
					else if (!txStatus.prepared())
					{
						logger.fine("Rolling back non prepared tx [" + name + "]" + "txIdent[" + identifier + "]");
						log.doneInternal(xaTransaction.getIdentifier());
						xidMap.Remove(xid);
						recoveredTxCount--;
					}
					else
					{
						logger.fine("2PC tx [" + name + "] " + txStatus + " txIdent[" + identifier + "]");
					}
				}
			}
			checkIfRecoveryComplete();
		}

		private void checkIfRecoveryComplete()
		{
			if (log.scanIsComplete() && recoveredTxCount == 0)
			{
			// log.makeNewLog();
				tf.recoveryComplete();
			}
		}

	// for testing, do not use!
		lock void reset()
		{
			xaResourceMap.Clear();
			xidMap.Clear();
			log.reset();
		}

///    
///     <summary> * Returns <CODE>true</CODE> if recovered transactions exist. This method
///     * is useful to invoke after the logical log has been opened to detirmine if
///     * there are any recovered transactions waiting for the TM to tell them what
///     * to do.
///     *  </summary>
///     * <returns> True if recovered transactions exist </returns>
///     
		public bool hasRecoveredTransactions()
		{
			return recoveredTxCount > 0;
		}
	}
}